<script lang="ts" setup>
import { getGallery, galleryImagesPage } from '@/api/gallery-upload'
import { queryLabels, createLabel, editLabel, deleteLabel } from '@/api/gallery/labels'
import { randomColor } from '@/utils/color'
import { isSegmentsIntersect } from '@/utils/geometry'
import { Plus } from '@element-plus/icons-vue'
import OpenSeadragon from 'openseadragon'
import Annotorious from '@cogic/annotorious-openseadragon'
import '@cogic/annotorious-openseadragon/dist/annotorious.min.css'
import BetterPolygon from '@cogic/annotorious-better-polygon/src'
import { parseRectFragment } from '@cogic/annotorious/src/selectors/RectFragment'
import { svgFragmentToShape } from '@cogic/annotorious/src/selectors/EmbeddedSVG'
import { addClass, hasClass, removeClass } from '@cogic/annotorious/src/util/SVG'
import LabelsFormatter from './labelsFormatter'

/** type interface class */

type selectorType = 'FragmentSelector' | 'SvgSelector'

interface PageData {
  list: Array<any>
  pageNo: number
  pageSize: number
  total: number
  loading: boolean
}
interface ImagePageData extends PageData {
  annotatedNum: number
}
interface LabelPageData extends PageData {
  list: Array<Label>
}
interface Tool {
  id: string
  label: string
  icon: string
  click?: Function
}

class Label {
  galleryId: number | null
  id: number | null
  name: string
  color: string
  flag: number
  sort: number | null
  editable: boolean

  constructor({
    galleryId = null,
    id = null,
    name = '',
    color = '',
    flag = 1,
    sort = null,
    editable = false
  } = {}) {
    this.galleryId = galleryId
    this.id = id
    this.name = name
    this.color = color
    this.flag = flag
    this.sort = sort
    this.editable = editable
  }

  init() {
    this.name = ''
    this.color = randomColor()
    this.editable = true
  }
}
class AnnotationBody {
  purpose: string
  type: string
  value: Label | null = null

  constructor({ purpose = 'tagging', type = 'TextualBody', value }) {
    this.purpose = purpose
    this.type = type
    this.value = value
  }
}
class AnnotationSelector {
  type: selectorType
  conformsTo: string
  value: string

  constructor({
    type = 'FragmentSelector' as selectorType,
    conformsTo = 'http://www.w3.org/TR/media-frags/',
    value = ''
  } = {}) {
    this.type = type
    this.conformsTo = conformsTo
    this.value = value
  }
}
class Annotation {
  ['@context']: string
  type: string
  id: string
  body: Array<AnnotationBody> = []
  target: { selector: AnnotationSelector; source: string }

  constructor({
    ['@context']: context = 'http://www.w3.org/ns/anno.jsonld',
    type = 'Annotation',
    id = '',
    body = [],
    target = { selector: {}, source: '' }
  } = {}) {
    this['@context'] = context
    this.type = type
    this.id = id
    this.body = body
    this.target = { selector: new AnnotationSelector(target.selector), source: target.source }
  }

  get label(): Label | null | undefined {
    return this.body.find((b) => b.purpose == 'tagging')?.value
  }

  set label(value) {
    const label = this.body.find((b) => b.purpose == 'tagging')
    if (label) {
      label.value = value || null
    }
  }
}
class Gallery {
  id: number
  name: string = ''
  imagePageData: ImagePageData = {
    list: [],
    pageNo: 1,
    pageSize: 20,
    total: 0,
    annotatedNum: 0,
    loading: false
  }
  labelPageData: LabelPageData = {
    list: [],
    pageNo: 1,
    pageSize: 100,
    total: 0,
    loading: false
  }
  curLabel: Label | null = null

  constructor(id) {
    this.id = id
  }

  async fetchInfo() {
    const resData = await getGallery({ id: this.id })
    this.name = resData.name
    this.imagePageData.annotatedNum = resData.markNum
    return Promise.resolve()
  }

  async fetchImagePage() {
    this.imagePageData.loading = true

    const resData = await galleryImagesPage({
      galleryId: this.id,
      pageNo: this.imagePageData.pageNo,
      pageSize: this.imagePageData.pageSize
    })
    resData.list.forEach((item: any) => {
      item.status = item.markId ? ImageStatusEnum.ANNOTATED : ImageStatusEnum.UNANNOTATED
    })

    this.imagePageData.loading = false
    this.imagePageData.list.push(...resData.list)
    this.imagePageData.total = resData.total
    return Promise.resolve()
  }

  async fetchLabelList() {
    this.labelPageData.loading = true
    const res = await queryLabels({
      galleryId: this.id,
      pageNo: this.labelPageData.pageNo,
      pageSize: this.labelPageData.pageSize
    })
    this.labelPageData.list = res.list
      ?.map((item) => new Label(item))
      .sort((a, b) => a.sort - b.sort)

    if (this.curLabel) {
      this.curLabel = this.labelPageData.list.find((label) => label.id === this.curLabel.id)
    }

    this.labelPageData.loading = false
    return Promise.resolve()
  }

  async submitLabel(label) {
    label.name = label.name.trim()
    if (!label.editable) {
      return
    } else if (label.name.length < 1) {
      return message.error('请输入标签名称')
    } else if (!label.color) {
      return message.error('请选择标签颜色')
    }

    let requestApi, successMsg
    if (label.id === null) {
      requestApi = createLabel
      successMsg = '标签添加成功'
    } else {
      requestApi = editLabel
      successMsg = '标签修改成功'
    }

    try {
      const lastLabel = this.labelPageData.list[this.labelPageData.list.length - 1]
      const nextSort = lastLabel?.sort == null ? 0 : lastLabel.sort + 1
      const res = await requestApi({
        galleryId: this.id,
        id: label.id,
        name: label.name,
        color: label.color,
        sort: label.sort == null ? nextSort : label.sort
      })
      if (res) {
        label.editable = false
        message.success(successMsg)
        this.fetchLabelList()
      } else {
        message.error('系统异常')
      }
    } catch (error) {
      message.error('系统异常')
    }
  }

  async removeLabel(label) {
    const res = label.id !== null && (await deleteLabel(label.id))
    if (res) {
      message.success('删除标签成功！')
      this.fetchLabelList()
    } else {
      message.error('系统异常')
    }
  }

  loadMoreImages() {
    if (this.imagePageData.loading || this.imagePageData.list.length >= this.imagePageData.total) {
      return
    }

    this.imagePageData.pageNo++
    return this.fetchImagePage()
  }
}
class ImageAnnotationTask {
  gallery: Gallery
  viewer: OpenSeadragon | null
  anno: Annotorious | null
  annotations: Array<Annotation> = []
  curDrawingTool: Tool | null = null
  curAnnotation: Annotation | null = null
  curImageIndex: number = -1
  imageFailed: boolean = false
  editableLabel: Label | null = null

  constructor(gallery) {
    this.gallery = gallery
  }

  initViewer(imageContainerElement) {
    this.destroyViewer()

    OpenSeadragon.setString('Tooltips.ZoomIn', imageControls.zoomInButton.label)
    OpenSeadragon.setString('Tooltips.ZoomOut', imageControls.zoomOutButton.label)
    OpenSeadragon.setString('Tooltips.Home', imageControls.homeButton.label)
    OpenSeadragon.setString('Tooltips.FullPage', imageControls.fullPageButton.label)

    this.viewer = OpenSeadragon({
      element: imageContainerElement,
      minZoomImageRatio: 0.1,
      maxZoomPixelRatio: 100000,
      animationTime: 0.5,
      zoomPerClick: 1.5,
      showNavigator: true,
      navigatorPosition: 'BOTTOM_LEFT',
      navigatorBorderColor: '#ccc',
      zoomInButton: imageControls.zoomInButton.id,
      zoomOutButton: imageControls.zoomOutButton.id,
      homeButton: imageControls.homeButton.id,
      fullPageButton: imageControls.fullPageButton.id,
      gestureSettingsMouse: {
        clickToZoom: false,
        dragToPan: true
      }
    })

    this.viewer.setVisible(false)

    this.viewer.addHandler('open-failed', (e) => {
      message.error(e.message)
      this.imageFailed = true
      this.viewer.setVisible(false)
    })
    this.viewer.addHandler('open', () => {
      this.imageFailed = false
      if (!this.viewer.isVisible()) {
        this.viewer.setVisible(true)
      }
    })
  }

  initAnno() {
    this.destroyAnno()

    const formatter = function (annotation) {
      annotation = new Annotation(annotation)
      const label = annotation.label
      if (label) {
        return {
          shapeStyle: `--label-color: ${label.color}; --text-color: ${textDiffColor(label.color)}`
        }
      }
    }

    const config = {
      disableEditor: true,
      allowEmpty: true,
      drawOnSingleClick: true,
      enableEdgeControls: true,
      handleRadius: 6,
      hotkey: 'null',
      crosshair: true,
      crosshairWithCursor: true,
      // Only works when drawOnSingleClick is true
      addPolygonPointOnMouseDown: true,
      // Whether to remove the original shape synchronously to avoid shape change bounce
      syncRemoveOriginalShape: true,
      // If true, no shapes will be set to hoverd when tools.current.enable is true
      disableHoverWhenToolEnabled: true,
      // For annotorious-better-polygon
      enableMultiPointSelection: false,
      // For annotorious-better-polygon
      hideMidpointOnSmallDistance: true,
      // For annotorious-better-polygon
      polygonCornerDeletable: true,
      formatters: [LabelsFormatter, formatter]
    }

    this.anno = Annotorious(this.viewer, config)
    BetterPolygon(this.anno)

    const eventListeners = {
      createAnnotation: (annotation) => {
        this.annotations.push(new Annotation(annotation))
        this.changeDrawingTool(this.curDrawingTool)

        this.onSaveAnnotation(annotation)
      },
      deleteAnnotation: (annotation) => {
        this.handleOnDeleteAnnotation(annotation.id)
      },
      createSelection: (selection) => {
        selection.body = [
          {
            type: 'TextualBody',
            purpose: 'tagging',
            value: this.curLabel
          }
        ]

        this.anno.updateSelected(selection, true)
      },
      selectAnnotation: (annotation) => {
        this.curAnnotation = new Annotation(annotation)
      },
      cancelSelected: (selection) => {
        this.curAnnotation = null
        this.onSaveAnnotation(selection)
      },
      updateAnnotation: (annotation) => {
        this.onSaveAnnotation(annotation)
      }
    }

    for (const key in eventListeners) {
      this.anno.on(key, eventListeners[key])
    }
  }

  openImage(imageUrl) {
    this.removeAllAnnotations()
    this.viewer.open({ type: 'image', url: imageUrl })
  }

  previousImage(imageTable) {
    if (this.curImageIndex > 0) {
      this.curImageIndex--
      const image = this.gallery.imagePageData.list[this.curImageIndex]

      imageTable.setCurrentRow(image)
      imageTable.$el
        .querySelector('.el-table__row.current-row')
        .scrollIntoView({ behavior: 'smooth', block: 'center' })
    }
  }

  nextImage(imageTable) {
    if (this.curImageIndex < this.gallery.imagePageData.list.length - 1) {
      this.curImageIndex++
      const image = this.gallery.imagePageData.list[this.curImageIndex]

      imageTable.setCurrentRow(image)
      imageTable.$el
        .querySelector('.el-table__row.current-row')
        .scrollIntoView({ behavior: 'smooth', block: 'center' })
    }
  }

  changeDrawingTool(tool) {
    if (tool === drawingTools.mouse) {
      this.anno.setDrawingEnabled(false, true)
      this.viewer.gestureSettingsMouse.dragToPan = true
    } else {
      const tempCurAnnotation = this.curAnnotation
      this.anno.saveSelected()
      this.anno.selectAnnotation()
      this.curAnnotation = null
      tempCurAnnotation && this.onSaveAnnotation(tempCurAnnotation)

      // setDrawingTool 须于 setDrawingEnabled 前执行，以使得 disableHoverWhenToolEnabled 生效
      this.anno.setDrawingTool(tool?.id)
      this.anno.setDrawingEnabled(true)
      this.viewer.gestureSettingsMouse.dragToPan = false
    }
    this.curDrawingTool = tool
  }

  changeLabel(label) {
    this.curLabel = label
  }

  selectAnnotation(annotationId) {
    this.anno.selectAnnotation(annotationId)
    this.curAnnotation = new Annotation(this.anno.getAnnotationById(annotationId))
  }

  removeAnnotation(annotationId) {
    this.anno.removeAnnotation(annotationId)
    this.handleOnDeleteAnnotation(annotationId)
  }

  removeAllAnnotations() {
    this.anno.clearAnnotations()
    this.annotations = []
  }

  handleOnDeleteAnnotation(annotationId) {
    this.annotations.splice(
      this.annotations.findIndex((a) => a.id === annotationId),
      1
    )
  }

  onSaveAnnotation(annotation) {
    annotation = new Annotation(annotation)

    const hasIntersectingEdges = (points) => {
      const n = points.length
      for (let i = 0; i < n; i++) {
        for (let j = i + 1; j < n; j++) {
          if (i === 0 && j === n - 1) continue
          const p1 = points[i]
          const p2 = points[(i + 1) % n]
          const p3 = points[j]
          const p4 = points[(j + 1) % n]
          if (isSegmentsIntersect(p1, p2, p3, p4)) {
            return true
          }
        }
      }
      return false
    }

    const shape = this.webAnnotationToLabelme([annotation]).shapes[0]
    if (shape.shape_type === 'polygon') {
      const points = shape.points.map((point) => ({ x: point[0], y: point[1] }))

      const shapeEl = document.querySelector(`.a9s-annotation[data-id='${annotation.id}']`)
      if (shapeEl && hasIntersectingEdges(points) && !hasClass(shapeEl, 'error-shape')) {
        addClass(shapeEl, 'error-shape')
      } else if (shapeEl && hasClass(shapeEl, 'error-shape')) {
        removeClass(shapeEl, 'error-shape')
      }
    }
  }

  webAnnotationToLabelme(annotations) {
    const image = this.anno._env.image
    const shapes = annotations.map((annotation) => {
      return {
        label: annotation.label.id,
        points: (() => {
          const selector = function (this: any) {
            return this.target.selector
          }

          if (annotation.target.selector.type === 'FragmentSelector') {
            const { x, y, w, h } = parseRectFragment({ ...annotation, selector })
            return [
              [x, y],
              [x + w, y + h]
            ]
          } else {
            return Array.from(svgFragmentToShape({ ...annotation, selector }).points).map(
              (point: any) => [point.x, point.y]
            )
          }
        })(),
        group_id: null,
        description: '',
        shape_type: (() => {
          if (annotation.target.selector.type === 'FragmentSelector') {
            return 'rectangle'
          } else {
            return 'polygon'
          }
        })(),
        flags: {},
        mask: null
      }
    })

    return {
      version: '0.0.0',
      flags: {},
      shapes: shapes || [],
      imagePath: image.src,
      imageData: null,
      imageHeight: image.naturalHeight,
      imageWidth: image.naturalWidth
    }
  }

  destroyViewer() {
    if (this.viewer) {
      this.viewer.destroy()
      this.viewer = null
    }
  }

  destroyAnno() {
    if (this.anno) {
      this.anno.destroy()
      this.anno = null
    }
  }

  get curLabel() {
    return this.gallery.curLabel
  }

  set curLabel(value) {
    this.gallery.curLabel = value
  }

  destroy() {
    this.destroyAnno()
    this.destroyViewer()
    this.annotations = []
    this.curAnnotation = null
  }
}

const ImageStatusEnum = {
  ANNOTATED: '已标注',
  UNANNOTATED: '未标注'
}
const drawingTools = {
  mouse: { id: 'mouse', label: '指针', icon: 'mdi:cursor-default-outline' } as Tool,
  rectangle: { id: 'rect', label: '矩形', icon: 'gis:rectangle-pt' } as Tool,
  polygon: { id: 'polygon', label: '多边形', icon: 'gis:polygon-pt' } as Tool
}
const handleTools = {
  save: { id: 'save', label: '保存', icon: 'mdi:content-save' } as Tool,
  reset: {
    id: 'reset',
    label: '清空标注',
    icon: 'mdi:brush-variant',
    click() {
      task.removeAllAnnotations()
    }
  } as Tool,
  delete: { id: 'delete', label: '删除图片', icon: 'mdi:file-image-remove-outline' } as Tool
}
const imageControls = {
  zoomInButton: { id: 'zoom-in-btn', label: '放大', icon: 'mdi:add' } as Tool,
  zoomOutButton: { id: 'zoom-out-btn', label: '缩小', icon: 'mdi:minus' } as Tool,
  homeButton: {
    id: 'home-btn',
    label: '还原大小',
    icon: 'fluent:auto-fit-width-24-filled'
  } as Tool,
  fullPageButton: {
    id: 'full-page-btn',
    label: '切换全屏',
    icon: 'icon-park-outline:stretching'
  } as Tool
}

const message = useMessage()
const routes = useRoute()
const router = useRouter()

const imageContainer = useTemplateRef('image-container')
const imageTable: any = useTemplateRef('image-table')

const queryRaw = JSON.parse(routes.query.row as unknown as string)

const gallery = reactive(new Gallery(queryRaw.id))
const labelFormData = reactive(new Label({ galleryId: gallery.id }))
const task = reactive(new ImageAnnotationTask(gallery))

/** directives */

// 输入框自动聚焦
const vElInputFocus = {
  mounted: (el) => el.querySelector('input').focus()
}
// 表格懒加载
const vElTableLoadMore = {
  mounted: (el, binding) => {
    const scrollBox = el.querySelector('.el-scrollbar__wrap')
    scrollBox.addEventListener('scroll', (evt) => {
      const { scrollHeight, scrollTop, clientHeight } = evt.target
      if (scrollHeight - scrollTop <= clientHeight + 40) {
        binding.value()
      }
    })
  }
}

/** methods */

// 根据背景色返回更适合显示的文本颜色
const textDiffColor = (bgColor, brightColor = '#fff', darkColor = '#303133') => {
  const color = bgColor
  const r = parseInt(color.slice(1, 3), 16)
  const g = parseInt(color.slice(3, 5), 16)
  const b = parseInt(color.slice(5, 7), 16)
  const brightness = 0.299 * r + 0.587 * g + 0.114 * b
  return brightness > 128 ? darkColor : brightColor
}

const gotoRun = () => {
  router.push({ name: 'runIndex' })
}

/** hooks */

onMounted(async () => {
  await task.gallery.fetchInfo()
  await task.gallery.fetchImagePage()
  await task.gallery.fetchLabelList()

  task.initViewer(imageContainer.value)
  task.initAnno()
  task.changeDrawingTool(drawingTools.mouse)
  task.changeLabel(task.gallery.labelPageData.list[0])

  const firstImage = task.gallery.imagePageData.list[0]
  if (firstImage) {
    imageTable.value.setCurrentRow(firstImage)
  }
})
</script>

<template>
  <div class="wrapper w-full h-full flex flex-row gap-2">
    <section class="left flex flex-col w-[320px] h-full gap-2">
      <el-card shadow="always">
        <ul class="project-info-list">
          <li
            class="info-item"
            v-for="infoItem in [
              { label: '项目ID：', value: '-' },
              { label: '项目名称：', value: '-' },
              { label: '图库ID：', value: task.gallery.id },
              { label: '当前图库：', value: task.gallery.name }
            ]"
            :key="infoItem.label"
          >
            <span class="label">{{ infoItem.label }}</span>
            <span class="value">{{ infoItem.value }}</span>
          </li>
        </ul>
      </el-card>

      <el-card class="flex-1" shadow="always">
        <div class="area-wrapper gallery-box">
          <div class="area-title">
            <Icon class="title-icon" icon="mdi:format-list-bulleted" :size="20" />
            <span>图片列表</span>
          </div>
          <div class="gallery-info">
            <div
              class="info-item"
              v-for="infoItem in [
                { label: '图片总数：', value: task.gallery.imagePageData.total },
                { label: '已标注数：', value: task.gallery.imagePageData.annotatedNum }
              ]"
              :key="infoItem.label"
              >{{ infoItem.label }}<span class="value">{{ infoItem.value }}</span></div
            >
          </div>
          <div class="divider my-2"></div>
          <div class="selects flex pb-6 gap-1">
            <el-select placeholder="请选择状态" clearable filterable>
              <el-option v-for="item in ImageStatusEnum" :key="item" :label="item" :value="item" />
            </el-select>
          </div>
          <el-table
            ref="image-table"
            class="image-table"
            current-change="handleCurrentChange"
            highlight-current-row
            :data="task.gallery.imagePageData.list"
            v-el-table-load-more="() => task.gallery.loadMoreImages()"
            @current-change="
              (row) => {
                task.curImageIndex = task.gallery.imagePageData.list.indexOf(row)
                task.openImage(row.fileUrl)
              }
            "
          >
            <el-table-column type="index" label="序号" min-width="40" />
            <el-table-column
              prop="name"
              label="图片名称"
              min-width="100"
              :show-overflow-tooltip="{ enterable: false }"
            />
            <el-table-column prop="status" label="图片状态" align="center" min-width="60">
              <template #default="scope">
                <el-tag :type="scope.row.status === ImageStatusEnum.ANNOTATED ? 'success' : 'info'">
                  {{ scope.row.status }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-card>
    </section>

    <section class="flex-1 h-full flex flex-col gap-2">
      <div class="tools w-full h-[60px]">
        <el-card class="w-full h-full">
          <div class="tool-group flex flex-justify-between h-full">
            <div class="flex h-full">
              <el-tooltip
                v-for="toolItem in drawingTools"
                :key="toolItem.id"
                effect="dark"
                :content="toolItem.label"
                placement="bottom"
                :show-after="800"
                :hide-after="0"
                :show-arrow="false"
                :enterable="false"
              >
                <div
                  class="tool-item"
                  :class="{ selected: task.curDrawingTool?.id === toolItem.id }"
                  @click="task.changeDrawingTool(toolItem)"
                >
                  <Icon :icon="toolItem.icon" :size="24" />
                </div>
              </el-tooltip>
            </div>
            <div class="flex h-full">
              <div
                v-for="toolItem in handleTools"
                :key="toolItem.id"
                class="tool-item handle-tool-item"
                @click="toolItem.click"
              >
                <Icon :icon="toolItem.icon" :size="20" />
                <span>{{ toolItem.label }}</span>
              </div>
            </div>
          </div>
        </el-card>
      </div>

      <div class="w-full flex-1">
        <el-card class="w-full h-full">
          <div class="h-full flex flex-col">
            <div
              ref="image-container"
              class="image-container w-full min-h-[10px] flex-1"
              :class="{ 'edit-mode': task.curDrawingTool?.id === drawingTools.mouse.id }"
            >
              <div
                v-show="task.curImageIndex >= 0 && !task.imageFailed"
                class="image-control-group"
              >
                <div
                  v-for="btn in imageControls"
                  :id="btn.id"
                  :key="btn.id"
                  class="image-control-item"
                >
                  <Icon :icon="btn.icon" :size="20" />
                </div>
              </div>
              <div class="image-container-tip">
                <div v-if="task.curImageIndex < 0" class="empty-tip">
                  <Icon icon="mdi:image-off" :size="40" />
                  <span>未选择图片</span>
                </div>
                <div v-if="task.imageFailed" class="empty-tip">
                  <Icon icon="mdi:image-broken-variant" :size="40" />
                  <span>图片加载失败</span>
                </div>
              </div>
            </div>

            <div class="image-bottom-box">
              <el-button
                type="primary"
                :disabled="task.curImageIndex <= 0"
                plain
                @click="task.previousImage(imageTable)"
              >
                <Icon icon="mdi:navigate-before" :size="20" />
              </el-button>
              <el-text class="image-name" truncated>
                {{ task.gallery.imagePageData.list[task.curImageIndex]?.name || '未选择图片' }}
              </el-text>
              <el-button
                type="primary"
                :disabled="task.curImageIndex >= task.gallery.imagePageData.total - 1"
                plain
                @click="task.nextImage(imageTable)"
              >
                <Icon icon="mdi:navigate-next" :size="20" />
              </el-button>
            </div>
          </div>
        </el-card>
      </div>

      <div class="h-[180px] w-full flex flex-row gap-2">
        <div class="flex-1">
          <el-card class="w-full h-full">
            <div class="area-title">参数设置</div>
          </el-card>
        </div>
      </div>
    </section>

    <section class="w-[320px] h-full flex flex-col gap-2">
      <div class="w-full h-[30%]">
        <el-card class="w-full h-full">
          <div class="area-wrapper label-list-area">
            <div class="area-title flex">
              <div class="mr-3">标签列表</div>
              <el-button
                v-if="!labelFormData.editable"
                class="flex-1"
                type="primary"
                plain
                :icon="Plus"
                @click="labelFormData.init()"
              >
                添加标签
              </el-button>
              <div v-else class="label-input-box flex-1 flex">
                <el-input
                  class="input-item flex-1"
                  v-model="labelFormData.name"
                  v-el-input-focus
                  placeholder="标签名"
                >
                  <template #prepend>
                    <el-color-picker v-model="labelFormData.color" class="color-picker" />
                  </template>
                </el-input>

                <div class="handle-btn-box">
                  <el-button
                    v-for="btn in [
                      {
                        type: 'success',
                        icon: 'mdi:check',
                        click: () => {
                          task.gallery.submitLabel(labelFormData)
                        }
                      },
                      {
                        type: 'default',
                        icon: 'mdi:close',
                        click: () => {
                          labelFormData.editable = false
                        }
                      }
                    ]"
                    :key="btn.icon"
                    class="handle-btn"
                    :type="btn.type as any"
                    @click="btn.click"
                  >
                    <Icon :icon="btn.icon" :size="20" />
                  </el-button>
                </div>
              </div>
            </div>

            <div v-if="task.gallery.labelPageData.list.length > 0" class="divider"></div>

            <div class="flex-1 min-h-[10px]">
              <el-scrollbar v-if="task.gallery.labelPageData.list.length > 0">
                <div class="label-group">
                  <div
                    v-for="label in task.gallery.labelPageData.list"
                    :key="label.id as number"
                    class="label-item"
                    :class="{ selected: task.curLabel === label, editable: label.editable }"
                    :style="{ '--label-color': label.color }"
                    @click="task.curLabel = label"
                  >
                    <div
                      v-if="!label.editable"
                      class="label-name"
                      :style="{ '--text-color': textDiffColor(label.color) }"
                    >
                      <Icon class="label-icon" icon="mdi:tag" :size="20" />
                      <el-text truncated :style="{ color: 'inherit' }">
                        {{ label.name }}
                      </el-text>
                    </div>
                    <el-input
                      v-else
                      class="input-item flex-1"
                      v-model="labelFormData.name"
                      v-el-input-focus
                      :disabled="!label.editable"
                      placeholder="标签名"
                      @click.stop
                    >
                      <template #prepend>
                        <el-color-picker
                          v-if="label.editable"
                          v-model="labelFormData.color"
                          class="color-picker"
                          @click.stop
                        />
                      </template>
                    </el-input>
                    <div class="handle-btn-box">
                      <template v-if="!label.editable">
                        <el-button
                          v-for="btn in [
                            {
                              type: 'default',
                              icon: 'mdi:eye',
                              click: () => {}
                            },
                            {
                              type: 'default',
                              icon: 'mdi:edit',
                              click: () => {
                                task.editableLabel && (task.editableLabel.editable = false)
                                task.editableLabel = label

                                label.editable = true
                                labelFormData.editable = false
                                labelFormData.color = label.color
                                labelFormData.name = label.name
                              }
                            },
                            {
                              type: 'danger',
                              icon: 'mdi:delete',
                              click: () => {
                                task.gallery.removeLabel(label)
                              }
                            }
                          ]"
                          :key="btn.icon"
                          class="handle-btn"
                          :type="btn.type as any"
                          text
                          @click.stop="btn.click"
                        >
                          <Icon :icon="btn.icon" :size="16" />
                        </el-button>
                      </template>
                      <template v-else>
                        <el-button
                          v-for="btn in [
                            {
                              type: 'success' as any,
                              icon: 'mdi:check',
                              click: () => {
                                const { name, color } = labelFormData
                                task.gallery.submitLabel(Object.assign(label, { name, color }))
                              }
                            },
                            {
                              type: 'default' as any,
                              icon: 'mdi:close',
                              click: () => {
                                label.editable = false
                              }
                            }
                          ]"
                          :key="btn.icon"
                          class="handle-btn"
                          :type="btn.type"
                          text
                          bg
                          @click.stop="btn.click"
                        >
                          <Icon :icon="btn.icon" :size="16" />
                        </el-button>
                      </template>
                    </div>
                  </div>
                </div>
              </el-scrollbar>

              <div v-else class="empty-tip">
                <span>暂无标签</span>
              </div>
            </div>
          </div>
        </el-card>
      </div>

      <div class="w-full flex-1 min-h-[10px]">
        <el-card class="w-full h-full">
          <div class="area-wrapper">
            <div class="area-title">标注列表（{{ task.annotations.length }}）</div>

            <div v-if="task.annotations.length > 0" class="divider"></div>

            <div class="flex-1 min-h-[10px]">
              <el-scrollbar v-if="task.annotations.length > 0">
                <div class="annotation-group">
                  <div
                    v-for="(annotation, index) in task.annotations"
                    :key="annotation.id"
                    :class="{
                      'annotation-item': true,
                      selected: annotation.id === task.curAnnotation?.id
                    }"
                    :style="{
                      '--text-color': textDiffColor(annotation.label?.color),
                      '--label-color': annotation.label?.color,
                      cursor:
                        task.curDrawingTool?.id === drawingTools.mouse.id ? 'pointer' : 'default'
                    }"
                    @click="
                      task.curDrawingTool?.id === drawingTools.mouse.id &&
                        task.selectAnnotation(annotation.id)
                    "
                  >
                    <Icon class="annotation-item-icon" icon="mdi:bookmark" :size="20" />
                    <div class="annotation-item-tag">
                      {{ index + 1 }}.&nbsp;
                      <el-select
                        v-if="annotation.label"
                        v-show="annotation.id === task.curAnnotation?.id"
                        v-model="annotation.label"
                        class="tag-select"
                        value-key="id"
                        filterable
                        @change="task.selectAnnotation(annotation.id)"
                      >
                        <el-option
                          v-for="label in task.gallery.labelPageData.list"
                          :key="label.id as number"
                          :label="label.name"
                          :value="label"
                        />
                      </el-select>
                      <span v-show="annotation.id !== task.curAnnotation?.id">{{
                        annotation.label?.name
                      }}</span>
                    </div>
                    <div class="handle-btn-box">
                      <el-button
                        v-for="btn in [
                          {
                            type: 'danger',
                            icon: 'mdi:delete',
                            click: () => {
                              task.removeAnnotation(annotation.id)
                            }
                          }
                        ]"
                        :key="btn.icon"
                        class="handle-btn"
                        :type="btn.type as any"
                        text
                        @click.stop="btn.click"
                      >
                        <Icon :icon="btn.icon" :size="16" />
                      </el-button>
                    </div>
                  </div>
                </div>
              </el-scrollbar>

              <div v-else class="empty-tip">
                <span>暂无标注</span>
              </div>
            </div>
          </div>
        </el-card>
      </div>

      <el-card>
        <div class="w-full h-full flex flex-col gap-4">
          <div
            v-for="(btns, index) in [
              [
                { label: '自动标注', disabled: true },
                { label: '检查图库', disabled: true }
              ],
              [
                { label: '类别统计', disabled: true },
                { label: '前往训练', click: gotoRun }
              ]
            ]"
            :key="index"
            class="flex flex-wrap justify-evenly"
          >
            <el-button
              v-for="btn in btns"
              :key="btn.label"
              type="primary"
              :disabled="btn.disabled"
              @click="btn.click"
              >{{ btn.label }}</el-button
            >
          </div>
        </div>
      </el-card>
    </section>
  </div>
</template>

<style lang="scss" scoped>
/** 全局 */

$transition-duration: 0.3s;
$raivas-blue: #0078dc;

.area-wrapper {
  display: flex;
  flex-direction: column;
  height: 100%;

  .area-title {
    display: flex;
    align-items: center;
    padding-bottom: 10px;

    .title-icon {
      margin-right: 10px;
    }
  }
}

.divider {
  width: 100%;
  height: 1px;
  border-top: 1px solid rgb(187 187 187);
  box-sizing: border-box;
}

.empty-tip {
  display: flex;
  height: 100%;
  color: #9a9a9a;
  border: 1px dashed #ccc;
  border-radius: 4px;
  justify-content: center;
  align-items: center;
}

:deep() {
  .el-card__body {
    height: 100%;
    padding: 10px;
  }
}

/** 项目信息 */

.project-info-list {
  .info-item {
    display: flex;
    padding: 6px 0;
    font-size: 14px;

    .label {
      flex-basis: 30%;
      flex-shrink: 0;
      margin-right: 10px;
      font-weight: bold;
      text-align: right;
    }

    .value {
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
}

/** 图片列表 */

.gallery-box {
  .image-table {
    flex: 1;
  }
}

.gallery-info {
  display: flex;
  justify-content: flex-end;

  .info-item {
    font-size: smaller;
    color: #9a9a9a;

    + .info-item {
      margin-left: 20px;
    }

    .value {
      color: #000;
    }
  }
}

/** 工具栏 */

.tool-group {
  .tool-item {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 50px;
    height: 100%;
    cursor: pointer;
    background-color: #f1f1f1;
    border: 1px solid #ccc;
    border-radius: 4px;
    transition: all $transition-duration;
    user-select: none;

    + .tool-item {
      margin-left: 10px;
    }

    &:hover {
      color: $raivas-blue;
      border: 1px solid #aaa;
      transform: scale(1.05);
      box-shadow: 0 0 5px 0 rgb(0 0 0 / 10%);
    }

    &:active {
      transform: scale(0.9);
    }

    &.selected {
      color: #fff;
      background-color: $raivas-blue;
      border: 1px solid #aaa;
    }

    &.handle-tool-item {
      display: flex;
      width: 60px;
      font-size: 14px;
      background-color: unset;
      border: none;
      transform: none;
      box-shadow: none;
      flex-direction: column;

      &:active {
        color: #000;
      }
    }
  }
}

/** 图像标注区 */

.image-container {
  position: relative;
  overflow: hidden;
  background-color: var(--el-fill-color-blank);
  border: 1px solid #ccc;
  border-radius: 4px;

  .image-control-group {
    position: absolute;
    top: 10px;
    left: 10px;
    display: flex;
    flex-direction: column;
    pointer-events: none;

    .image-control-item {
      z-index: 1;
      display: flex !important;
      width: 36px;
      height: 36px;
      font-size: 20px;
      cursor: pointer;
      background-color: #ddd;
      border-radius: 50%;
      opacity: 0;
      box-shadow: 0 0 5px 0 rgb(0 0 0 / 53.4%);
      transition: all 0.3s;
      align-items: center;
      justify-content: center;

      + .image-control-item {
        margin-top: 10px;
      }

      &:hover {
        color: $raivas-blue;
        transform: scale(1.05);
      }

      &:active {
        transform: scale(0.9);
      }
    }
  }

  .image-container-tip {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    padding: 10px;
  }

  &:hover .image-control-group .image-control-item {
    opacity: 1;
  }
}

.image-bottom-box {
  display: flex;
  margin-top: 10px;
  align-items: center;
  justify-content: space-between;

  .image-name {
    width: 10px;
    padding: 0 10px;
    flex-grow: 1;
    text-align: center;
  }
}

/** 标签列表 */

.label-list-area {
  :deep(.color-picker) .el-color-picker__trigger {
    border: none;

    .el-color-picker__color {
      overflow: hidden;
      border: none;
      border-radius: 7px;
    }
  }

  .input-item {
    :deep(.el-input-group__prepend) {
      padding: 0;
    }
  }

  .handle-btn-box {
    .handle-btn {
      width: 32px;
      margin-left: 5px;
    }
  }

  .label-group {
    padding: 5px 10px;

    .label-item {
      display: flex;
      padding: 1px;
      border-radius: 4px;
      transition: all $transition-duration;

      + .label-item {
        margin-top: 10px;
      }

      .label-name {
        display: flex;
        min-width: 10px;
        padding: 0 10px;
        flex-grow: 1;
        align-items: center;

        .label-icon {
          margin-right: 5px;
          color: var(--label-color);
        }

        span {
          cursor: default;
        }
      }

      .handle-btn-box {
        display: flex;
        margin-left: 5px;
        background-color: rgb(255 255 255 / 70%);
        border-radius: 4px;
        opacity: 0;
        transition: opacity $transition-duration;
        align-items: center;

        .handle-btn {
          width: 32px;
          margin-left: 0;

          + .handle-btn {
            margin-left: 5px;
          }
        }
      }

      &:hover {
        background-color: #f1f1f1;

        .handle-btn-box {
          opacity: 1;
        }
      }

      &.selected {
        position: sticky;
        top: 5px;
        bottom: 5px;
        z-index: 1;
        background-color: var(--label-color);
        box-shadow: 0 0 0 1px #ccc;

        .label-name {
          color: var(--text-color);

          .label-icon {
            color: inherit;
          }
        }
      }

      &.editable {
        .handle-btn-box {
          opacity: 1;
        }
      }
    }
  }
}

/** 标注列表 */
.annotation-group {
  padding: 5px 10px;

  .annotation-item {
    display: flex;
    padding: 2px 2px 2px 10px;
    border-radius: 4px;
    transition: all 0.3s;
    align-items: center;
    justify-content: space-between;

    + .annotation-item {
      margin-top: 5px;
    }

    .annotation-item-icon {
      color: var(--label-color);
    }

    .annotation-item-tag {
      display: flex;
      align-items: center;
      flex-grow: 1;
      margin-left: 10px;

      .tag-select {
        margin: 0 5px;
      }
    }

    .handle-btn-box {
      display: flex;
      margin-left: 5px;
      background-color: rgb(255 255 255 / 70%);
      border-radius: 4px;
      opacity: 0;
      transition: opacity $transition-duration;
      align-items: center;

      .handle-btn {
        width: 32px;
        margin-left: 0;

        + .handle-btn {
          margin-left: 5px;
        }
      }
    }

    &:hover {
      background-color: #f1f1f1;

      .handle-btn-box {
        opacity: 1;
      }
    }

    &.selected {
      position: sticky;
      top: 5px;
      bottom: 5px;
      z-index: 1;
      color: var(--text-color);
      background-color: var(--label-color);
      box-shadow: 0 0 0 1px #ccc;

      .annotation-item-icon {
        color: inherit;
      }
    }
  }
}
</style>

<style lang="scss">
.openseadragon-canvas {
  outline: none;
}

.a9s-selection:not(.improved-polygon),
.a9s-selection.improved-polygon {
  .a9s-outer {
    stroke: rgb(0 0 0 / 35%);
    stroke-width: 3px;
  }

  .a9s-inner {
    stroke: #fff;
    stroke-dasharray: 5 3;
    stroke-width: 2px;
  }
}

.a9s-annotation {
  &.error-shape {
    .a9s-inner {
      fill: red !important;
      stroke: red !important;
    }
  }

  /* stylelint-disable-next-line no-descending-specificity */
  .a9s-outer {
    display: none;
  }

  /* stylelint-disable-next-line no-descending-specificity */
  .a9s-inner {
    fill: var(--label-color);
    fill-opacity: 0.1;
    stroke: var(--label-color) !important;
    stroke-width: 1.6;
    transition: fill-opacity 0.2s;
  }

  .a9s-formatter-el foreignObject .a9s-shape-label-wrapper .a9s-shape-label {
    color: var(--text-color);
    background-color: var(--label-color);
    border: 1px solid var(--label-color);
    opacity: 0.8;
    transition:
      opacity 0.2s,
      background-color 0.2s;
  }

  &.hover {
    .a9s-inner {
      fill-opacity: 0.3;
    }

    .a9s-formatter-el foreignObject .a9s-shape-label-wrapper .a9s-shape-label {
      opacity: 1;
    }
  }

  &.editable:not(.improved-polygon),
  &.editable.improved-polygon {
    .a9s-inner {
      fill-opacity: 0.4;
      stroke-dasharray: 5 3;

      &:hover {
        fill: var(--label-color);
        fill-opacity: 0.3;
      }
    }

    .a9s-handle {
      .a9s-handle-outer,
      .a9s-handle-inner {
        transition:
          fill 0.2s,
          fill-opacity 0.2s,
          stroke 0.2s,
          stroke-width 0.2s;
      }

      &:not(.selected),
      &.selected {
        .a9s-handle-inner {
          fill: var(--label-color);
          stroke: #000;
          stroke-width: 1;
        }
      }

      &:hover {
        .a9s-handle-outer {
          fill: transparent;
          stroke: #fff;
          stroke-width: 3;
        }

        .a9s-handle-inner {
          fill-opacity: 0.3;
          stroke-width: 2;
        }
      }

      &:active {
        .a9s-handle-outer {
          fill: transparent;
          stroke: #fff;
          stroke-width: 3;
        }

        .a9s-handle-inner {
          cursor: none;
          fill-opacity: 0.1;
          stroke-width: 2;
        }

        ~ .a9s-handle {
          .a9s-handle-inner {
            cursor: none;
          }
        }
      }

      &:hover,
      &:active {
        &.deletable {
          .a9s-handle-inner {
            fill: red;
            fill-opacity: 1;
          }
        }
      }
    }

    .a9s-formatter-el foreignObject .a9s-shape-label-wrapper .a9s-shape-label {
      border-color: #000;
      opacity: 1;
    }
  }
}

.image-container {
  .a9s-crosshair line {
    stroke: rgb(255 0 0 / 50%);
    shape-rendering: auto;
  }

  &.edit-mode {
    .a9s-crosshair {
      display: none;
    }
  }

  &:not(.edit-mode) {
    .a9s-annotationlayer {
      cursor: crosshair !important;

      * {
        cursor: crosshair !important;
      }
    }
  }
}
</style>
